--[===================================================================[--
   ffi_accel - Mod for Minetest to accelerate handling of LuaVoxelManip
               and PerlinNoiseMap through the foreign function interface

   Copyright © 2019 Pedro Gimeno Fortea. All rights reserved.

   Permission is hereby granted to everyone to copy, modify, distribute
   and use this file, for any purpose, in whole or in part, free of
   charge, under the sole condition that the above copyright notice,
   together with this permission grant and the disclaimer below, are
   included in all copies of this software or of a substantial portion
   of it.

   THIS SOFTWARE COMES WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED.
--]===================================================================]--

-- Based on an idea of EvidenceBKidsCode.
-- This mod needs to apply a patch to the engine in order to work; see
--

local modname = "Mod ffi_accel"

local err_prefix = modname .. " won't have any effect because "

local ie = minetest.request_insecure_environment()
if not ie then
  minetest.log("warning", err_prefix .. "it's not declared in secure.trusted_mods")
  return
end

local ok, ffi = pcall(ie.require, 'ffi')
if not ok or not ffi then
  minetest.log("warning", err_prefix .. "FFI is not available in this system")
  return
end

ffi.cdef[[
  typedef uint8_t u8;
  typedef uint16_t u16;
  typedef int32_t s32;
  typedef struct {
    u16 nodeid;
    u8 light;
    u8 param2;
  } MapNode;

  int get_mapnode_version(void);
  void VoxelManip_get_pointer(void **lvmp, void **ptr);
  s32 VoxelManip_get_volume(void **lvmp);
  void PerlinNoiseMap_get_pointer(void **pnmp, void **ptr);
  size_t PerlinNoiseMap_get_area(void **pnmp);
  size_t PerlinNoiseMap_get_volume(void **pnmp);
]]

local ffiC = ffi.C
local ffinew = ffi.new
local VoidPtrPtr = ffi.typeof('void*[1]')
local MapNodePtr = ffi.typeof('MapNode *')

local mt_set_data, mt_set_light_data, mt_set_param2_data

if not pcall(function ()
  assert(ffiC.get_mapnode_version and ffiC.VoxelManip_get_pointer
    and ffiC.VoxelManip_get_volume and ffiC.PerlinNoiseMap_get_pointer)
  end)
then
  minetest.log("warning", err_prefix .. "your Minetest executable has not been patched to support FFI")
  return
end

if ffiC.get_mapnode_version() ~= 1 then
  minetest.log("warning", err_prefix .. "of incompatible map node structure")
  return
end


-- Table of pointers (keys are weak refs)
local ptrtable = setmetatable({}, {__mode = 'k'})
-- Table of sizes (keys are weak refs)
local sizetable = setmetatable({}, {__mode = 'k'})


-- Metamethod: get a value
local function mm_get(t, idx)
  return idx >= 1 and idx <= sizetable[t] and ptrtable[t][idx - 1] or rawget(t, idx)
end


-- Metamethod: set a value
local function mm_set(t, idx, value)
  if idx >= 1 and idx <= sizetable[t] then
    ptrtable[t][idx - 1] = value
  else
    rawset(t, idx, value)
  end
end

-- Metamethod: get the nodeid of an index
local function mm_get_nodeid(t, idx)
  return idx >= 1 and idx <= sizetable[t] and ptrtable[t][idx - 1].nodeid or rawget(t, idx)
end

-- Metamethod: set the nodeid of an index to a value
local function mm_set_nodeid(t, idx, value)
  if idx >= 1 and idx <= sizetable[t] then
    ptrtable[t][idx - 1].nodeid = value
  else
    rawset(t, idx, value)
  end
end

-- Metamethod: get the light of an index
local function mm_get_light(t, idx)
  return idx >= 1 and idx <= sizetable[t] and ptrtable[t][idx - 1].light or rawget(t, idx)
end

-- Metamethod: set the light of an index to a value
local function mm_set_light(t, idx, value)
  if idx >= 1 and idx <= sizetable[t] then
    ptrtable[t][idx - 1].light = value
  else
    rawset(t, idx, value)
  end
end

-- Metamethod: get the param2 of an index
local function mm_get_param2(t, idx)
  return idx >= 1 and idx <= sizetable[t] and ptrtable[t][idx - 1].param2 or rawget(t, idx)
end

-- Metamethod: set the param2 of an index to a value
local function mm_set_param2(t, idx, value)
  if idx >= 1 and idx <= sizetable[t] then
    ptrtable[t][idx - 1].param2 = value
  else
    rawset(t, idx, value)
  end
end


-- Metatables for VoxelManip nodeid, light and param2
local VoxelManip_nodeid_mt = { __index = mm_get_nodeid, __newindex = mm_set_nodeid }
local VoxelManip_light_mt = { __index = mm_get_light, __newindex = mm_set_light }
local VoxelManip_param2_mt = { __index = mm_get_param2, __newindex = mm_set_param2 }


-- Replacement VoxelManip.get_data and VoxelManip.set_data methods
local function VoxelManip_get_data(self, buffer)
  -- Set up the metatable and pointer
  buffer = setmetatable(buffer or {}, VoxelManip_nodeid_mt)
  do
    local ptr = VoidPtrPtr()
    ffiC.VoxelManip_get_pointer(self, ptr)
    ptrtable[buffer] = MapNodePtr(ptr[0])
  end
  sizetable[buffer] = ffiC.VoxelManip_get_volume(self)
  return buffer
end

local function VoxelManip_set_data(self, buffer)
  -- Call the original set_data method if the metatable is not
  -- the one we've set
  if getmetatable(buffer) ~= VoxelManip_nodeid_mt then
    mt_set_data(self, buffer)
  end
end


-- Replacement VoxelManip.get/set_light_data methods
local function VoxelManip_get_light_data(self, buffer)
  -- Set up the metatable and pointer
  buffer = setmetatable(buffer or {}, VoxelManip_light_mt)
  do
    local ptr = VoidPtrPtr()
    ffiC.VoxelManip_get_pointer(self, ptr)
    ptrtable[buffer] = MapNodePtr(ptr[0])
  end
  sizetable[buffer] = ffiC.VoxelManip_get_volume(self)
  return buffer
end

local function VoxelManip_set_light_data(self, buffer)
  -- Call the original set_light_data method if the metatable is not
  -- the one we've set
  if getmetatable(buffer) ~= VoxelManip_light_mt then
    mt_set_light_data(self, buffer)
  end
end


-- Replacement VoxelManip.get/set_param2_data methods
local function VoxelManip_get_param2_data(self, buffer)
  -- Set up the metatable and pointer
  buffer = setmetatable(buffer or {}, VoxelManip_param2_mt)
  do
    local ptr = VoidPtrPtr()
    ffiC.VoxelManip_get_pointer(self, ptr)
    ptrtable[buffer] = MapNodePtr(ptr[0])
  end
  sizetable[buffer] = ffiC.VoxelManip_get_volume(self)
  return buffer
end

local function VoxelManip_set_param2_data(self, buffer)
  -- Call the original set_param2_data method if the metatable is not
  -- the one we've set
  if getmetatable(buffer) ~= VoxelManip_param2_mt then
    mt_set_param2_data(self, buffer)
  end
end


-- Metatable for PerlinNoiseMap
local PerlinNoiseMap_mt = { __index = mm_get }

-- Replacement methods for PerlinNoiseMap.*
local function PerlinNoiseMap_get_2d_map_flat(self, pos, buffer)
  -- Calculate the noise
  self:calc_2d_map(pos)

  -- Prepare the metatable for direct access to the data
  buffer = setmetatable(buffer or {}, PerlinNoiseMap_mt)
  do
    local ptr = VoidPtrPtr()
    ffiC.PerlinNoiseMap_get_pointer(self, ptr)
    ptrtable[buffer] = MapNodePtr(ptr[0])
  end
  sizetable[buffer] = ffiC.PerlinNoiseMap_get_area(self)
  return buffer
end

local function PerlinNoiseMap_get_3d_map_flat(self, pos, buffer)
  -- Calculate the noise
  self:calc_3d_map(pos)

  -- Prepare the metatable for direct access to the data
  buffer = setmetatable(buffer or {}, PerlinNoiseMap_mt)
  do
    local ptr = VoidPtrPtr()
    ffiC.PerlinNoiseMap_get_pointer(self, ptr)
    ptrtable[buffer] = MapNodePtr(ptr[0])
  end
  sizetable[buffer] = ffiC.PerlinNoiseMap_get_volume(self)
  return buffer
end

--[[
local function PerlinNoiseMap_get_map_slice(slice_offset, slice_size, buffer)
   -- TODO
end
--]]

-- Monkey-patch VoxelManip and PerlinNoiseMap
minetest.after(0, function()
  -- VoxelManip buffer-intensive methods
  local mt = getmetatable(minetest.get_voxel_manip())
  mt.get_data = VoxelManip_get_data
  mt_set_data = mt.set_data
  mt.set_data = VoxelManip_set_data
  mt.get_light_data = VoxelManip_get_light_data
  mt_set_light_data = mt.set_light_data
  mt.set_light_data = VoxelManip_set_light_data
  mt.get_param2_data = VoxelManip_get_param2_data
  mt_set_param2_data = mt.set_param2_data
  mt.set_param2_data = VoxelManip_set_param2_data

  -- PerlinNoiseMap buffer-intensive methods
  mt = getmetatable(minetest.get_perlin_map({spread = {x=100, y=100, z=100}}, {x=1, y=1, z=1}))
  mt.get_2d_map_flat = PerlinNoiseMap_get_2d_map_flat
  mt.get_3d_map_flat = PerlinNoiseMap_get_3d_map_flat
--  mt.get_map_slice = PerlinNoiseMap_get_map_slice

  -- Aliases
  mt.get2DMap_flat = mt.get_2d_map_flat
  mt.get3DMap_flat = mt.get_3d_map_flat
--  mt.getMapSlice = mt.get_map_slice
end)

minetest.log("action", modname .. " activated!")
